package com.mspbots.web.security.service;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.mspbots.api.BadRequestException;
import com.mspbots.api.RestServerException;
import com.mspbots.api.page.Page;
import com.mspbots.web.BaseService;
import com.mspbots.web.params.ChangPasswordRequest;
import com.mspbots.web.params.UserParam;
import com.mspbots.web.security.User;
import com.mspbots.web.security.UserOauth;
import com.mspbots.web.security.repositories.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.query.Criteria;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.security.Principal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.Map;
import java.util.Objects;

/**
 * com.mspbots.web.security.UsersService
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2019/9/10
 */
@Log4j2
@Service
@RequiredArgsConstructor
public class UsersService extends BaseService {

    private final String CACHING_USER_KEY_PREFIX = "caching:web:user:";
    private final PasswordEncoder passwordEncoder;

    private final DatabaseClient databaseClient;
    private final UserRepository userRepository;

    public Mono<User> loadUser(String username) {
        Assert.notNull(username, () -> "username is required and cannot be empty.");
        Mono<String> identifier = this.databaseClient.execute("SELECT identifier FROM tenant_user WHERE email ILIKE :email LIMIT 1")
                .bind("email", username).map((row, rowMetadata) -> row.get("identifier", String.class))
                .one()
                .defaultIfEmpty("");
        return this.reactiveRedisTemplate.opsForValue().get(this.CACHING_USER_KEY_PREFIX + username)
                .map(user ->{
                    User u = this.objectMapper.convertValue(user, User.class);
                    if (!ObjectUtils.isEmpty(u.getRemoved()) && "null".equalsIgnoreCase(u.getRemoved().asText())){
                        u.setRemoved(null);
                    }
                    return u;
                })
                .switchIfEmpty(this.userRepository.findByUsername(username).cache(Duration.ofMinutes(30))
                        .flatMap(user -> identifier.map(id -> {
                            user.setIdentifier(id);
                            if (!ObjectUtils.isEmpty(user.getRemoved()) && "null".equalsIgnoreCase(user.getRemoved().asText())){
                                user.setRemoved(null);
                            }
                            return user;
                        }))
                        .doOnNext(user -> this.reactiveRedisTemplate.opsForValue()
                                .set(this.CACHING_USER_KEY_PREFIX + username, user, Duration.ofHours(2))
                                .subscribe())
                );
    }

    public Mono<User> loadUserByUsername(String username) {
        Assert.notNull(username, () -> "username is required and cannot be empty.");
        return this.userRepository.findByUsername(username);
    }


    public Mono<User> loadTeamsUser(String username) {
        Assert.notNull(username, () -> "username is required and cannot be empty.");
        return this.userRepository.findByUsername(username)
                .flatMap(user -> this.databaseClient
                        .execute("SELECT cw_uid,identifier FROM tenant_user WHERE email ILIKE :email AND tenant_id =:tenant_id LIMIT 1")
                        .bind("email", username)
                        .bind("tenant_id", user.getTenantId())
                        .map((row, rowMetadata) -> Map.of("identifier", Objects.requireNonNull(row.get("identifier")),
                                "id", Objects.requireNonNull(row.get("cw_uid"))))
                        .one()
                        .defaultIfEmpty(Map.of()).map(idMap -> {
                            user.setIdentifier(idMap.getOrDefault("identifier", "").toString());
                            user.setCwUid(idMap.getOrDefault("id", "").toString());
                            log.debug("teams login: {}", user);
                            return user;
                        }));
    }

    @Transactional(rollbackFor = Exception.class)
    public Mono<Integer> setLastLogin(String username) {
        Assert.notNull(username, () -> "User entry is required and cannot be empty.");
        return this.userRepository.setLastLogin(username);
    }

    @Transactional(rollbackFor = Exception.class)
    public Mono<User> changePassword(ChangPasswordRequest requestParams) {
        return ReactiveSecurityContextHolder.getContext()
                .map(SecurityContext::getAuthentication)
                .filter(Objects::nonNull)
                .switchIfEmpty(Mono.error(new AccessDeniedException("Can't change password as no Authentication object found in context for current user.")))
                .map(Principal::getName)
                .flatMap(username -> this.loadUser(username)
                        .filter(user -> this.passwordEncoder.matches(requestParams.getPassword(), user.getPassword()))
                        .switchIfEmpty(Mono.error(new AuthService.PasswordIncorrectException("This request old password is error.")))
                        .flatMap(user -> {
                            String newPassword = this.passwordEncoder.encode(requestParams.getNewPassword());
                            user.setPassword(newPassword);
                            user.setModifiedTime(LocalDateTime.now());
                            return this.userRepository.save(user);
                        }).doOnSuccess(user -> {
                            UserDetails userDetails = org.springframework.security.core.userdetails.User.withUsername(user.getUsername())
                                    .password(user.getPassword())
                                    .authorities(user.getAuthorities().toArray(new String[0]))
                                    .accountExpired(!user.getEnabled())
                                    .credentialsExpired(!user.getEnabled())
                                    .disabled(!user.getEnabled())
                                    .accountLocked(!user.getEnabled())
                                    .build();
                            UsernamePasswordAuthenticationToken newAuthentication = new UsernamePasswordAuthenticationToken(
                                    userDetails, null, userDetails.getAuthorities());
                            newAuthentication.setDetails(user);
                            SecurityContextHolder.getContext().setAuthentication(newAuthentication);
                        })
                );

    }

    @Transactional(rollbackFor = Exception.class)
    public Mono<User> changeUser(UserParam request) {

        return this.loadUser(request.getUsername())
                .flatMap(user -> {
                    if (!StringUtils.isEmpty(request.getEnable())) {
                        user.setEnabled(request.getEnable());
                    }
                    if (!StringUtils.isEmpty(request.getEmail())) {
                        user.setEmail(request.getEmail());
                    }
                    if (!StringUtils.isEmpty(request.getFirstName())) {
                        user.setFirstName(request.getFirstName());
                    }
                    if (!StringUtils.isEmpty(request.getLastName())) {
                        user.setLastName(request.getLastName());
                    }
                    if (!StringUtils.isEmpty(request.getPassword())) {
                        user.setPassword(this.passwordEncoder.encode(request.getPassword()));
                    }
                    if (!StringUtils.isEmpty(request.getRoles()) && request.getRoles().size() > 0) {
                        user.setAuthorities(request.getRoles());
                    }
                    if (!StringUtils.isEmpty(request.getManagerUserId())) {
                        user.setManagerUserId(request.getManagerUserId());
                    }
                    return this.userRepository.save(user);
                });
    }

    @Transactional(rollbackFor = Exception.class)
    public Mono<User> register(UserParam request) {
        User user = User.of(request.getUsername().toLowerCase(),
                this.passwordEncoder.encode(request.getPassword()), request.getRoles(), true);
        user.setTenantId(request.getTenantId());
        user.setEmail(request.getEmail());
        user.setFirstName(request.getFirstName());
        user.setLastName(request.getLastName());
        return this.userRepository.save(user);

    }

    public Mono<Page<User>> loadAllPage(User user, Page<User> pageable) {

        StringBuilder stringBuffer = new StringBuilder("where username is not null ");
        if (!ObjectUtils.isEmpty(user.getUsername())) {
            stringBuffer.append("and username ilike '%").append(user.getUsername().toLowerCase()).append("%' ");
        }
        if (!ObjectUtils.isEmpty(user.getEmail())) {
            stringBuffer.append("and email ilike  '%").append(user.getEmail()).append("%' ");
        }
        if (!ObjectUtils.isEmpty(user.getTenantId())) {
            stringBuffer.append("and tenant_id = ").append(user.getTenantId()).append(" ");
        }

        if (!ObjectUtils.isEmpty(user.getEnabled())) {
            stringBuffer.append("and enabled = ").append(user.getEnabled()).append(" ");
        }
        if (!ObjectUtils.isEmpty(user.getFirstName())) {
            stringBuffer.append("and first_name||' '||last_name ilike '%").append(user.getFirstName()).append("%' ");
        }

        stringBuffer.append("and removed is null ").append(" ");

        var all = this.databaseClient.execute("select * from users " + stringBuffer.toString() + " order by username limit "
                + pageable.getSize() + " offset  " + (pageable.getCurrent() - 1) * pageable.getSize())
                .as(User.class)
                .fetch().all();

        var totalMono = this.databaseClient.execute("select count(*) from users " + stringBuffer.toString())
                .as(Integer.class)
                .fetch().one();

        return all.collectList()
                .map(pageable::setRecords)
                .flatMap(p -> totalMono.map(total -> p.setTotal(NumberUtils.parseNumber(total + "", Integer.class))))
                .onErrorResume(e -> Mono.error(new BadRequestException("Get users page is error.")));
    }

    public Mono<User> save(User user) {
        Assert.notNull(user, () -> "User entry is required and cannot be empty.");
        return this.userRepository.save(user);
    }

    public Flux<User> loadAll(User user) {
        Criteria query = Criteria.where("username").isNotNull();
        if (!ObjectUtils.isEmpty(user.getUsername())) {
            query = query.and("username").like("%" + user.getUsername().toLowerCase() + "%");
        }
        if (!ObjectUtils.isEmpty(user.getEmail())) {
            query = query.and("email").like("%" + user.getEmail().toLowerCase() + "%");
        }
        if (!ObjectUtils.isEmpty(user.getTenantId())) {
            query = query.and("tenant_id").is(user.getTenantId());
        }
        return this.databaseClient.select().from(User.class)
                .matching(query)
                .orderBy(Sort.by("username"))
                .as(User.class).all();
    }

    public Mono<User> loadByTenantLast(Integer tenantId) {
        return this.userRepository.findByTenantLast(tenantId);
    }

    public Flux<UserOauth> authList(Integer tenantId) {
        Criteria query = Criteria.where("username").isNotNull()
                .and("tenant_id").is(tenantId)
                .and("enabled").is(true);

        return this.databaseClient.select().from(User.class)
                .matching(query)
                .orderBy(Sort.by("username"))
                .as(User.class).all()
                .map(u -> {
                    UserOauth userOauth = new UserOauth();
                    BeanUtils.copyProperties(u, userOauth);
                    if (!CollectionUtils.isEmpty(u.getAuthorities()) && u.getAuthorities().contains("ROLE_TENANT_ADMIN")) {
                        userOauth.setIsAdmin(true);
                    }
                    return userOauth;
                }).sort(Comparator.comparing(UserOauth::getIsAdmin).reversed());
    }

    static class SaveUserProgressException extends RestServerException {

        SaveUserProgressException(String message) {
            this(500, message);
        }

        SaveUserProgressException(int code, String msg) {
            super(code, msg);
        }
    }


}
